<- function(a, b) {
f2 * 10
a
}f2(10, stop("This is an error!"))
6 Functions
Introduction
在学习本章之前,你一定已经写过很多用来减少重复工作的函数,本章会将你在工作中的一些知识进行整合提升,帮助你从理论的视角看待函数。在本章,你会看到一些有趣的小技巧和复杂技术,请一定要留心,这些内容是后面章节中的基石。
Quiz
- 下面的代码运行时会报错吗?
- 无论运行成功或失败,函数如何都执行一些操作?
Outline
- 6.2节:构成函数的三要素和原始函数(primitive function)。
- 6.3节:函数联合使用的三种方法及其优劣。
- 6.4节:词法作用域规则(rules of lexical scoping)——如何根据名字找到对应的值。
- 6.5节:参数评估原则:只在第一次使用时评估,避免循环。
- 6.6节:特殊参数
...
。 - 6.7节:函数退出机制。
- 6.8节:四种函数格式。
Function fundamentals
Function components
函数由三部分组成:
formals()
:参数和函数名,控制如何调用函数。body()
:函数具体实现。environment()
:环境,决定函数如何找到参数对应的值。
在这三部分中,formals和body十分显性,构建出函数时就可以直观地看到;environment相对隐性,需要使用函数environment()
。
<- function(x, y) {
f02 # A comment
+ y
x
}
formals(f02)
#> $x
#>
#>
#> $y
body(f02)
#> {
#> x + y
#> }
environment(f02)
#> <environment: R_GlobalEnv>
与R中的其他对象一样,函数也会拥有一些属性。一个常见的属性是srcref
(source reference),记录了函数的源代码,如下所示。
attr(f02, "srcref")
#> NULL
Primitive functions
原始函数(primitive function)是一个例外,不包含上面的三要素。
sum#> function (..., na.rm = FALSE) .Primitive("sum")
`[`
#> .Primitive("[")
它们的base type
不同。(base type 见第12章)
typeof(sum)
#> [1] "builtin"
typeof(`[`)
#> [1] "special"
因为它们的底层是C语言,所以formals()
、body()
和environment()
返回值都是NULL
。
formals(sum)
#> NULL
body(sum)
#> NULL
environment(sum)
#> NULL
First-class functions
所谓的first-class
,就是指函数本身就是对象,可以用来赋值给变量,可以传递给函数等等。创建函数只需要function()
来定义,使用<-
进行绑定。
如果一个函数没有绑定名字,那么它就是匿名函数。匿名函数通常用在lapply()
等函数中,用来实现高效的数据处理。
lapply(mtcars, function(x) length(unique(x)))
Filter(function(x) !is.numeric(x), mtcars)
integrate(function(x) sin(x)^2, 0, pi)
有关R函数的闭包特性,见第7章。
Invoking a function
使用函数的常见方式是myfun(param1, param2, ...)
,如果你有了一组参数数据,可以使用do.call()
来调用函数。
<- list(1:10, na.rm = TRUE)
args do.call(mean, args)
#> [1] 5.5
Exercises
- 使用
is.function()
来判断一个对象是否是函数。对于原始函数,使用is.primitive()
。
Function composition
base R 提供了两种函数组合使用的方式。例如,当你想使用sqrt()
和mean()
计算某个群体的方差:
<- runif(100)
x <- function(x) x^2
square <- function(x) x - mean(x) deviation
第一种方法:
sqrt(mean(square(deviation(x))))
#> [1] 0.2744786
第二种方法:
<- deviation(x)
out <- square(out)
out <- mean(out)
out <- sqrt(out)
out
out#> [1] 0.2744786
magrittr包提供了第三种方式——管道符%>%
(R 4.0 之后可以之间使用 |>
代替)。
library(magrittr)
%>%
x deviation() %>%
square() %>%
mean() %>%
sqrt()
#> [1] 0.2744786
Lexical scoping
在第二章中,我们讲到为对象命名;在这里,我们介绍它的反面,根据名字找到对象——词法作用域(lexical scoping)。
R 的词法作用域遵循下面四个规则:
- Name masking——函数内部的变量优先于函数外部的变量。
- Functions versus variables——当某个函数与变量同名时,R自动找到对应的对象。
- A fresh start——函数每次执行都是独立的。
- Dynamic lookup——函数只有在运行时才会查找对应的对象。
Name masking
函数查找变量时,首先查找函数内的变量,然后再查找函数外,即——由内到外逐级查找,直至找到为止。
<- 10
x <- 20
y <- 30
z <- function() {
g05 <- 1
x <- 2
y c(x, y, z)
}g05()
#> [1] 1 2 30
Functions versus variables
诚如上述,函数也是普通的对象,在进行函数的查找时,遵循相同的规则。
<- function(x) x + 1
g07 <- function() {
g08 <- function(x) x + 100
g07 g07(10)
}g08()
#> [1] 110
如果,函数名和变量名重复(函数和变量在不同的环境中)时,R会自动找到对应类型的对象,例如下面的g09
。实际编写代码时,我们要尽可能避免这种情况的发生,因为十分歧义和迷惑。
<- function(x) x + 100
g09 <- function() {
g10 <- 10
g09 g09(g09)
}g10()
#> [1] 110
A fresh start
下面的例子,每次运行g11()
结果都是相同的,因为每次运行函数时,都会创建一个变量域(环境),它们之间相互独立。
<- function() {
g11 if (!exists("a")) {
<- 1
a else {
} <- a + 1
a
}
a
}
g11()
#> [1] 1
g11()
#> [1] 1
当你使用a <- g11()
时,就打破了独立。
<- g11()
a g11()
#> [1] 2
g11()
#> [1] 2
Dynamic lookup
函数只有在运行时才会根据名字查找对象,也即前后环境不一致时,函数前后运行值也就不一样。
<- function() x + 1
g12 <- 15
x g12()
#> [1] 16
<- 20
x g12()
#> [1] 21
使用codetools::findGlobals()
可以里列出函数内的外部依赖项。
::findGlobals(g12)
codetools#> [1] "+" "x"
environment(g12) <- emptyenv()
g12()
#> Error in x + 1: could not find function "+"
Exercises
…
Lazy evaluation
R 函数中的参数具有延迟评估(Lazy evaluation)特点:只有参数被使用时,才会对其进行评估(运行)。
<- function(x) {
h01 10
}h01(stop("This is an error!"))
#> [1] 10
这一特性允许解析耗时的参数只有在函数运行且被调用时进行解析。
Promises
延迟评估由一种被称为promise
或thunk
的数据结构赋能(这种数据结构,本人也不是很了解,可以GPT一下)。
promise有三种类型:
表达式,如
1 + 1
。创建的环境,如:函数自己的变量域。
<- 10
y <- function(x) {
h02 <- 100
y + 1
x
}
h02(y)
#> [1] 11
h02(y <- 1000)
#> [1] 1001
y#> [1] 1000
- 无需重复计算的值,如下面的
message
结果只打印一次。
<- function(x) {
double message("Calculating...")
* 2
x
}
<- function(x) {
h03 c(x, x)
}
h03(double(20))
#> [1] 40 40
<- double(20)
x h03(x)
#> [1] 40 40
promise
类型的延迟评估具有一种“薛定谔的猫”的特点,任何尝试用R去评估它的操作都会破坏其promise
特性。
Default arguments
延迟评估,允许函数在设置默认参数时,引用函数内部变量域,例如下面的例子。在base R中的许多函数都有类似的应用,但是不推荐这样使用,这样会增加函数理解的困难。
<- function(x = 1, y = x * 2, z = a + b) {
h04 <- 10
a <- 100
b
c(x, y, z)
}
h04()
#> [1] 1 2 110
另外一个需要注意的是:将要延迟评估的表达式
,“作为默认参数”和“直接传递给函数”,是两种不同的情况。下面的示例中,ls()
作为默认参数时,评估的是函数内部的变量域,而不是外部环境。
<- function(x = ls()) {
h05 <- 1
a
x
}
# ls() evaluated inside h05:
h05()
#> [1] "a" "x"
h05(ls())
#> [1] "a" "args" "deviation" "double" "f02" "g05"
#> [7] "g07" "g08" "g09" "g10" "g11" "g12"
#> [13] "h01" "h02" "h03" "h04" "h05" "out"
#> [19] "square" "status" "x" "y" "z"
Missing arguments
函数missing()
可以用来判断函数参数值来源,若来自于默认值,那么返回TRUE
,否则返回FALSE
。
<- function(x = 10) {
h06 list(missing(x), x)
}str(h06())
#> List of 2
#> $ : logi TRUE
#> $ : num 10
str(h06(10))
#> List of 2
#> $ : logi FALSE
#> $ : num 10
Exercises
- 下面代码发生的过程:
- promise
x = {y <- 1; 2}
在函数f1
自己创建的环境中被评估,赋值1给y
,返回数值2。 - promise最终的运行结果——2,赋值给函数参数
x
。 - 因为Name masking,不使用函数默认值,使用先前赋值为1的
y
。 - 因为函数内部评估不影响外部变量,所以最外面的
y
仍为10。
- promise
<- 10
y <- function(x = {
f1 <- 1
y 2
y = 0) {
}, c(x, y)
}f1()
#> [1] 2 1
y#> [1] 10
...
(dot-dot-dot)
...
是R函数的一个特殊参数,它使得R函数可以有任意数目的参数。
应用...
...
主要应用在下面两种情况:
- 要传递额外参数给另外一个函数。
<- function(y, z) {
i01 list(y = y, z = z)
}
<- function(x, ...) {
i02 i01(...)
}
str(i02(x = 1, y = 2, z = 3))
#> List of 2
#> $ y: num 2
#> $ z: num 3
# 常见的apply家族函数
<- list(c(1, 3, NA), c(4, NA, 6))
x str(lapply(x, mean, na.rm = TRUE))
#> List of 2
#> $ : num 2
#> $ : num 5
- S3面向对象中的方法函数,如下面的
print()
函数,对于不同类使用不同参数。S3面向对象详见第13章。
print(factor(letters), max.levels = 4)
#> [1] a b c d e f g h i j k l m n o p q r s t u v w x y z
#> 26 Levels: a b c ... z
print(y ~ x, showEnv = TRUE)
#> y ~ x
#> <environment: R_GlobalEnv>
解析...
- 可以使用
..N
的形式,来访问...
中的第N
个参数。
<- function(...) {
i03 list(first = ..1, third = ..3)
}str(i03(1, 2, 3))
#> List of 2
#> $ first: num 1
#> $ third: num 3
- 可以使用
list(...)
将其转换为list,储存起来。
<- function(...) {
i04 list(...)
}str(i04(a = 1, b = 2))
#> List of 2
#> $ a: num 1
#> $ b: num 2
- rlang包提供了额外的解析方法。
::list2()
rlang::enquos() rlang
Exiting a function
大多数函数的退出机制有两种:
- 显性或隐性的返回一个值,表示运行成功。
- 抛出错误信息,表示运行失败。
Implicit versus explicit returns
- 使用
return()
指定返回值。 - 如果不使用
return()
指定返回值,默认使用最后运行代码的值作为返回值。
<- function(x) {
j01 if (x < 10) {
0
else {
} 10
}
}j01(5)
#> [1] 0
j01(15)
#> [1] 10
<- function(x) {
j02 if (x < 10) {
return(0)
else {
} return(10)
}
}j02(5)
#> [1] 0
j02(15)
#> [1] 10
Invisible values
如果没有将函数的返回值赋值给某个变量,函数会将结果打印出来。
<- function() 1
j03 <- j03()
x j03()
#> [1] 1
使用invisible()
可以阻止函数自动打印。
<- function() invisible(1)
j04 j04()
print(j04())
#> [1] 1
j04())
(#> [1] 1
使用withVisible()
可以获取函数的返回值和是否可见。
str(withVisible(j04()))
#> List of 2
#> $ value : num 1
#> $ visible: logi FALSE
最常见的隐藏返回值的函数就是<-
。
<- 2
a <- 2)
(a #> [1] 2
Errors
当函数运行失败时,应当使用stop()
函数抛出错误信息,并终止函数运行。抛出错误信息是为了让使用者知道函数运行失败的原因,以及如何处理。
<- function() {
j05 stop("I'm an error")
return(10)
}j05()
#> Error in j05(): I'm an error
Exit handlers
在函数处理过程中,经常会有更新当前工作路径、绘图参数等全局变量,在函数运行结束后又要复原这些全局变量的操作。此时可以使用on.exit()
函数来添加函数在退出时的操作。下面的示例显示了:无论函数运行成功还是失败,on.exit()
函数都会执行。
<- function(x) {
j06 cat("Hello\n")
on.exit(cat("Goodbye!\n"), add = TRUE)
if (x) {
return(10)
else {
} stop("Error")
}
}
j06(TRUE)
#> Hello
#> Goodbye!
#> [1] 10
j06(FALSE)
#> Hello
#> Error in j06(FALSE): Error
on.exit()
函数的另外两个参数:
add
:当有多个退出操作时,如果add
为FALSE
则新的操作会覆盖原来的操作,推荐总是设置为TRUE
。after
:当有多个退出操作时,如果after
为FALSE
,新的操作会最先执行。
<- function() {
j08 on.exit(message("a"), add = TRUE)
on.exit(message("b"), add = FALSE)
}j08()
<- function() {
j09 on.exit(message("a"), add = TRUE, after = TRUE)
on.exit(message("b"), add = TRUE, after = TRUE)
on.exit(message("c"), add = TRUE, after = FALSE)
}j09()
Exercises
了解一下sink()
,capture.output()
函数。
Function forms
R 里面的两句slogan:
- Everything that exists is an object.
- Everything that happens is a function call. — John Chambers
R 中的函数有四种变体:
- prefix:函数名在参数前,例如
mean(x)
。 - infix:函数名在参数之间,例如
x + y
的+
;可以使用%
前后包裹函数名,进行自定义。 - replacement:带有
<-
赋值操作的函数,例如names(df) <- c("a", "b")
。 - special:例如
[[
、if
、for
等。
Rewriting to prefix form
任何形式的函数都可以改写成prefix
形式。
+ y
x `+`(x, y)
names(df) <- c("x", "y", "z")
`names<-`(df, c("x", "y", "z"))
for (i in 1:10) print(i)
`for`(i, 1:10, print(i))
R 的这种特性,可以让你随意地更改R中的基本函数。下面是一个更新了(
函数的例子,大约10次中有1次,返回值会加1。
`(` <- function(e1) {
if (is.numeric(e1) && runif(1) < 0.1) {
+ 1
e1 else {
}
e1
}
}replicate(50, (1 + 2))
#> [1] 3 3 3 3 3 3 3 3 3 3 3 3 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
#> [37] 3 3 4 3 4 3 3 3 3 4 3 3 3 3
rm("(")
Prefix form
prefix 格式的函数可以通过下面三种方式检索参数(也是检索优先级,由高至低):
- 使用参数名。
- 使用参数名部分匹配。
- 使用参数位置。
<- function(abcdef, bcde1, bcde2) {
k01 list(a = abcdef, b1 = bcde1, b2 = bcde2)
}
str(k01(1, 2, 3))
str(k01(2, 3, abcdef = 1)) 3
# Can abbreviate long argument names:
str(k01(2, 3, a = 1))
# But this doesn't work because abbreviation is ambiguous
str(k01(1, 3, b = 1))
#> Error in parse(text = input): <text>:2:28: unexpected numeric constant
#> 1: str(k01(1, 2, 3))
#> 2: str(k01(2, 3, abcdef = 1)) 3
#> ^
通常使用位置的参数是函数参数中最常用的几个,不推荐使用部分匹配设置参数。很遗憾,在R中无法禁用模糊匹配,但可以设置options(warnPartialMatchArgs = TRUE)
,生成警告信息。
options(warnPartialMatchArgs = TRUE)
<- k01(a = 1, 2, 3) x
Infix form
infix 格式的函数要求只能有两个参数。在base R中有许多这种格式的函数::
, ::
, :::
, $
, @
, ^
, *
, /
, +
, -
, >
, >=
, <
, <=
, ==
, !=
, !
, &
, &&
, |
, ||
, ~
, <-
, and <<-
。也可以使用%
前后包裹函数名,进行自定义。例如%*%
,%in%
等。
`%+%` <- function(a, b) paste0(a, b)
"new " %+% "string"
#> [1] "new string"
%
之间的函数名可以是除%
外的任意字符,需要转义的字符只需在定义时进行转义,使用时无须转义。
`% %` <- function(a, b) paste(a, b)
`%/\\%` <- function(a, b) paste(a, b)
"a" % % "b"
#> [1] "a b"
"a" %/\% "b"
#> [1] "a b"
infix 格式的函数总是将其左右两端的参数作为输入。
`%-%` <- function(a, b) paste0("(", a, " %-% ", b, ")")
"a" %-% "b" %-% "c"
#> [1] "((a %-% b) %-% c)"
Replacement form
replacement 格式的函数要求:
- 至少两个参数,分别是待赋值的对象和值。
- 必须返回更新值后的对象。
`second<-` <- function(x, value) {
2] <- value
x[
x
}
<- 1:10
x second(x) <- 5L
x#> [1] 1 5 3 4 5 6 7 8 9 10
如果你要添加额外参数,需要将其放置在x
和value
之间。
`modify<-` <- function(x, position, value) {
<- value
x[position]
x
}modify(x, 1) <- 10
x#> [1] 10 5 3 4 5 6 7 8 9 10
使用tracemem()
追踪内存地址的变化。
<- 1:10
x tracemem(x)
#> [1] "<0x5586ac400b08>"
second(x) <- 6L
#> tracemem[0x5586ac400b08 -> 0x5586b3037368]: eval eval withVisible withCallingHandlers eval eval with_handlers doWithOneRestart withOneRestart withRestartList doWithOneRestart withOneRestart withRestartList withRestarts <Anonymous> evaluate in_dir in_input_dir eng_r block_exec call_block process_group withCallingHandlers <Anonymous> process_file <Anonymous> <Anonymous> execute .main
#> tracemem[0x5586b3037368 -> 0x5586b3036488]: second<- eval eval withVisible withCallingHandlers eval eval with_handlers doWithOneRestart withOneRestart withRestartList doWithOneRestart withOneRestart withRestartList withRestarts <Anonymous> evaluate in_dir in_input_dir eng_r block_exec call_block process_group withCallingHandlers <Anonymous> process_file <Anonymous> <Anonymous> execute .main
Sepcial forms
下面是一些特殊格式的函数和它转换为prefix后的格式
special form | prefix form |
---|---|
(x) | `(`(x) |
{x} | `{`(x) |
x[i] | `[`(x, i) |
x[[i]] | `[[`(x, i) |
if (cond) true | `if`(cond, true) |
if (cond) true else false | `if`(cond, true, false) |
for(var in seq) action | `for`(var, seq, action) |
while (cond) action | `while`(cond, action) |
repeat expr | `repeat`(expr) |
next | `next`() |
break | `break`() |
function(arg1, arg2) {body} | `function`(alist(arg1, arg2), body, env ) |
Exercises
…